import {
  turnOptions,
  isTouch,
  mouseEvents,
  pagesInDOM,
  displays,
  directions,
} from "./utils/constant";
import { has, translate, hasHardPage, trigger, divAtt } from "./utils";
import { flipMethods } from "./flipMethods";
import TurnJsError from "./utils/TurnJsError";

const $ = window.$;

export const turnMethods = {
  // Singleton constructor
  // $('#selector').turn([options]);

  init: function (options) {
    // Define constants

    var i,
      pageNum = 0,
      data = this.data(),
      ch = this.children();

    // Set initial configuration
    options = $.extend(
      {
        width: this.width(),
        height: this.height(),
        direction: this.attr("dir") || this.css("direction") || "ltr",
      },
      turnOptions,
      options
    );

    data.opts = options;
    data.pageObjs = {};
    data.pages = {};
    data.pageWrap = {};
    data.pageZoom = {};
    data.pagePlace = {};
    data.pageMv = [];
    data.zoom = 1;
    data.totalPages = options.pages || 0;
    data.eventHandlers = {
      touchStart: $.proxy(turnMethods._touchStart, this),

      touchMove: $.proxy(turnMethods._touchMove, this),

      touchEnd: $.proxy(turnMethods._touchEnd, this),

      start: $.proxy(turnMethods._eventStart, this),
    };

    // Add event listeners

    if (options.when)
      for (i in options.when)
        if (has(i, options.when)) this.bind(i, options.when[i]);

    // Set the css

    this.css({
      position: "relative",
      width: options.width,
      height: options.height,
    });

    // Set the initial display

    this.turn("display", options.display);

    // Set the direction

    if (options.direction !== "") this.turn("direction", options.direction);

    // Prevent blue screen problems of switching to hardware acceleration mode
    // By forcing hardware acceleration for ever

    if (!isTouch && options.acceleration) this.transform(translate(0, 0, true));

    // Add pages from the DOM

    for (i = 0; i < ch.length; i++) {
      if ($(ch[i]).attr("ignore") != "1") {
        this.turn("addPage", ch[i], ++pageNum);
      }
    }

    // Event listeners

    $(this)
      .bind(mouseEvents.down, data.eventHandlers.touchStart)
      .bind("end", turnMethods._eventEnd)
      .bind("pressed", turnMethods._eventPressed)
      .bind("released", turnMethods._eventReleased)
      .bind("flip", turnMethods._flip);

    $(this).parent().bind("start", data.eventHandlers.start);

    $(document)
      .bind(mouseEvents.move, data.eventHandlers.touchMove)
      .bind(mouseEvents.up, data.eventHandlers.touchEnd);

    // Set the initial page

    this.turn("page", options.page);

    // This flipbook is ready

    data.done = true;

    return this;
  },

  // Adds a page from external data

  addPage: function (element, page) {
    var currentPage,
      className,
      incPages = false,
      data = this.data(),
      lastPage = data.totalPages + 1;

    if (data.destroying) return false;

    // Read the page number from the className of `element` - format: p[0-9]+

    if ((currentPage = /\bp([0-9]+)\b/.exec($(element).attr("class"))))
      page = parseInt(currentPage[1], 10);

    if (page) {
      if (page == lastPage) incPages = true;
      else if (page > lastPage)
        throw new TurnJsError('Page "' + page + '" cannot be inserted');
    } else {
      page = lastPage;
      incPages = true;
    }

    if (page >= 1 && page <= lastPage) {
      if (data.display == "double") className = page % 2 ? " odd" : " even";
      else className = "";

      // Stop animations
      if (data.done) this.turn("stop");

      // Move pages if it's necessary
      if (page in data.pageObjs) turnMethods._movePages.call(this, page, 1);

      // Increase the number of pages
      if (incPages) data.totalPages = lastPage;

      // Add element

      data.pageObjs[page] = $(element)
        .css({ float: "left" })
        .addClass("page p" + page + className);

      if (!hasHardPage() && data.pageObjs[page].hasClass("hard")) {
        data.pageObjs[page].removeClass("hard");
      }

      // Add page
      turnMethods._addPage.call(this, page);

      // Remove pages out of range
      turnMethods._removeFromDOM.call(this);
    }

    return this;
  },

  // Adds a page

  _addPage: function (page) {
    var data = this.data(),
      element = data.pageObjs[page];

    if (element)
      if (turnMethods._necessPage.call(this, page)) {
        if (!data.pageWrap[page]) {
          // Wrapper

          data.pageWrap[page] = $("<div/>", {
            class: "page-wrapper",
            page: page,
            css: { position: "absolute", overflow: "hidden" },
          });

          // Append to this flipbook
          this.append(data.pageWrap[page]);

          if (!data.pagePlace[page]) {
            data.pagePlace[page] = page;
            // Move `pageObjs[page]` to wrapper
            data.pageObjs[page].appendTo(data.pageWrap[page]);
          }

          // Set the size of the page
          var prop = turnMethods._pageSize.call(this, page, true);
          element.css({ width: prop.width, height: prop.height });
          data.pageWrap[page].css(prop);
        }

        if (data.pagePlace[page] == page) {
          // If the page isn't in another place, create the flip effect
          turnMethods._makeFlip.call(this, page);
        }
      } else {
        // Place
        data.pagePlace[page] = 0;

        // Remove element from the DOM
        if (data.pageObjs[page]) data.pageObjs[page].remove();
      }
  },

  // Checks if a page is in memory

  hasPage: function (page) {
    return has(page, this.data().pageObjs);
  },

  // Centers the flipbook

  center: function (page) {
    var data = this.data(),
      size = $(this).turn("size"),
      left = 0;

    if (!data.noCenter) {
      if (data.display == "double") {
        var view = this.turn("view", page || data.tpage || data.page);

        if (data.direction == "ltr") {
          if (!view[0]) left -= size.width / 4;
          else if (!view[1]) left += size.width / 4;
        } else {
          if (!view[0]) left += size.width / 4;
          else if (!view[1]) left -= size.width / 4;
        }
      }

      $(this).css({ marginLeft: left });
    }

    return this;
  },

  // Destroys the flipbook

  destroy: function () {
    var flipbook = this,
      data = this.data(),
      events = [
        "end",
        "first",
        "flip",
        "last",
        "pressed",
        "released",
        "start",
        "turning",
        "turned",
        "zooming",
        "missing",
      ];

    if (trigger("destroying", this) == "prevented") return;

    data.destroying = true;

    $.each(events, function (index, eventName) {
      flipbook.unbind(eventName);
    });

    this.parent().unbind("start", data.eventHandlers.start);

    $(document)
      .unbind(mouseEvents.move, data.eventHandlers.touchMove)
      .unbind(mouseEvents.up, data.eventHandlers.touchEnd);

    while (data.totalPages !== 0) {
      this.turn("removePage", data.totalPages);
    }

    if (data.fparent) data.fparent.remove();

    if (data.shadow) data.shadow.remove();

    this.removeData();
    data = null;

    return this;
  },

  // Checks if this element is a flipbook

  is: function () {
    return typeof this.data().pages == "object";
  },

  // Sets and gets the zoom value

  zoom: function (newZoom) {
    var data = this.data();

    if (typeof newZoom == "number") {
      if (newZoom < 0.001 || newZoom > 100)
        throw new TurnJsError(newZoom + " is not a value for zoom");

      if (trigger("zooming", this, [newZoom, data.zoom]) == "prevented")
        return this;

      var size = this.turn("size"),
        currentView = this.turn("view"),
        iz = 1 / data.zoom,
        newWidth = Math.round(size.width * iz * newZoom),
        newHeight = Math.round(size.height * iz * newZoom);

      data.zoom = newZoom;

      $(this).turn("stop").turn("size", newWidth, newHeight);
      /*.
        css({marginTop: size.height * iz / 2 - newHeight / 2});*/

      if (data.opts.autoCenter) this.turn("center");
      /*else
        $(this).css({marginLeft: size.width * iz / 2 - newWidth / 2});*/

      turnMethods._updateShadow.call(this);

      for (var i = 0; i < currentView.length; i++) {
        if (currentView[i] && data.pageZoom[currentView[i]] != data.zoom) {
          this.trigger("zoomed", [
            currentView[i],
            currentView,
            data.pageZoom[currentView[i]],
            data.zoom,
          ]);

          data.pageZoom[currentView[i]] = data.zoom;
        }
      }

      return this;
    } else return data.zoom;
  },

  // Gets the size of a page

  _pageSize: function (page, position) {
    var data = this.data(),
      prop = {};

    if (data.display == "single") {
      prop.width = this.width();
      prop.height = this.height();

      if (position) {
        prop.top = 0;
        prop.left = 0;
        prop.right = "auto";
      }
    } else {
      var pageWidth = this.width() / 2,
        pageHeight = this.height();

      if (data.pageObjs[page].hasClass("own-size")) {
        prop.width = data.pageObjs[page].width();
        prop.height = data.pageObjs[page].height();
      } else {
        prop.width = pageWidth;
        prop.height = pageHeight;
      }

      if (position) {
        var odd = page % 2;
        prop.top = (pageHeight - prop.height) / 2;

        if (data.direction == "ltr") {
          prop[odd ? "right" : "left"] = pageWidth - prop.width;
          prop[odd ? "left" : "right"] = "auto";
        } else {
          prop[odd ? "left" : "right"] = pageWidth - prop.width;
          prop[odd ? "right" : "left"] = "auto";
        }
      }
    }

    return prop;
  },

  // Prepares the flip effect for a page

  _makeFlip: function (page) {
    var data = this.data();

    if (!data.pages[page] && data.pagePlace[page] == page) {
      var single = data.display == "single",
        odd = page % 2;

      data.pages[page] = data.pageObjs[page]
        .css(turnMethods._pageSize.call(this, page))
        .flip({
          page: page,
          next: odd || single ? page + 1 : page - 1,
          turn: this,
        })
        .flip("disable", data.disabled);

      // Issue about z-index
      turnMethods._setPageLoc.call(this, page);

      data.pageZoom[page] = data.zoom;
    }

    return data.pages[page];
  },

  // Makes pages within a range

  _makeRange: function () {
    var page,
      range,
      data = this.data();

    if (data.totalPages < 1) return;

    range = this.turn("range");

    for (page = range[0]; page <= range[1]; page++)
      turnMethods._addPage.call(this, page);
  },

  // Returns a range of pages that should be in the DOM
  // Example:
  // - page in the current view, return true
  // * page is in the range, return true
  // Otherwise, return false
  //
  // 1 2-3 4-5 6-7 8-9 10-11 12-13
  //   **  **  --   **  **

  range: function (page) {
    var remainingPages,
      left,
      right,
      view,
      data = this.data();

    page = page || data.tpage || data.page || 1;
    view = turnMethods._view.call(this, page);

    if (page < 1 || page > data.totalPages)
      throw new TurnJsError('"' + page + '" is not a valid page');

    view[1] = view[1] || view[0];

    if (view[0] >= 1 && view[1] <= data.totalPages) {
      remainingPages = Math.floor((pagesInDOM - 2) / 2);

      if (data.totalPages - view[1] > view[0]) {
        left = Math.min(view[0] - 1, remainingPages);
        right = 2 * remainingPages - left;
      } else {
        right = Math.min(data.totalPages - view[1], remainingPages);
        left = 2 * remainingPages - right;
      }
    } else {
      left = pagesInDOM - 1;
      right = pagesInDOM - 1;
    }

    return [
      Math.max(1, view[0] - left),
      Math.min(data.totalPages, view[1] + right),
    ];
  },

  // Detects if a page is within the range of `pagesInDOM` from the current view

  _necessPage: function (page) {
    if (page === 0) return true;

    var range = this.turn("range");

    return (
      this.data().pageObjs[page].hasClass("fixed") ||
      (page >= range[0] && page <= range[1])
    );
  },

  // Releases memory by removing pages from the DOM

  _removeFromDOM: function () {
    var page,
      data = this.data();

    for (page in data.pageWrap)
      if (has(page, data.pageWrap) && !turnMethods._necessPage.call(this, page))
        turnMethods._removePageFromDOM.call(this, page);
  },

  // Removes a page from DOM and its internal references

  _removePageFromDOM: function (page) {
    var data = this.data();

    if (data.pages[page]) {
      var dd = data.pages[page].data();

      flipMethods._moveFoldingPage.call(data.pages[page], false);

      if (dd.f && dd.f.fwrapper) dd.f.fwrapper.remove();

      data.pages[page].removeData();
      data.pages[page].remove();
      delete data.pages[page];
    }

    if (data.pageObjs[page]) data.pageObjs[page].remove();

    if (data.pageWrap[page]) {
      data.pageWrap[page].remove();
      delete data.pageWrap[page];
    }

    turnMethods._removeMv.call(this, page);

    delete data.pagePlace[page];
    delete data.pageZoom[page];
  },

  // Removes a page

  removePage: function (page) {
    var data = this.data();

    // Delete all the pages
    if (page == "*") {
      while (data.totalPages !== 0) {
        this.turn("removePage", data.totalPages);
      }
    } else {
      if (page < 1 || page > data.totalPages)
        throw new TurnJsError("The page " + page + " doesn't exist");

      if (data.pageObjs[page]) {
        // Stop animations
        this.turn("stop");

        // Remove `page`
        turnMethods._removePageFromDOM.call(this, page);

        delete data.pageObjs[page];
      }

      // Move the pages
      turnMethods._movePages.call(this, page, -1);

      // Resize the size of this flipbook
      data.totalPages = data.totalPages - 1;

      // Check the current view

      if (data.page > data.totalPages) {
        data.page = null;
        turnMethods._fitPage.call(this, data.totalPages);
      } else {
        turnMethods._makeRange.call(this);
        this.turn("update");
      }
    }

    return this;
  },

  // Moves pages

  _movePages: function (from, change) {
    var page,
      that = this,
      data = this.data(),
      single = data.display == "single",
      move = function (page) {
        var next = page + change,
          odd = next % 2,
          className = odd ? " odd " : " even ";

        if (data.pageObjs[page])
          data.pageObjs[next] = data.pageObjs[page]
            .removeClass("p" + page + " odd even")
            .addClass("p" + next + className);

        if (data.pagePlace[page] && data.pageWrap[page]) {
          data.pagePlace[next] = next;

          if (data.pageObjs[next].hasClass("fixed"))
            data.pageWrap[next] = data.pageWrap[page].attr("page", next);
          else
            data.pageWrap[next] = data.pageWrap[page]
              .css(turnMethods._pageSize.call(that, next, true))
              .attr("page", next);

          if (data.pages[page])
            data.pages[next] = data.pages[page].flip("options", {
              page: next,
              next: single || odd ? next + 1 : next - 1,
            });

          if (change) {
            delete data.pages[page];
            delete data.pagePlace[page];
            delete data.pageZoom[page];
            delete data.pageObjs[page];
            delete data.pageWrap[page];
          }
        }
      };

    if (change > 0)
      for (page = data.totalPages; page >= from; page--) move(page);
    else for (page = from; page <= data.totalPages; page++) move(page);
  },

  // Sets or Gets the display mode

  display: function (display) {
    var data = this.data(),
      currentDisplay = data.display;

    if (display === undefined) {
      return currentDisplay;
    } else {
      if ($.inArray(display, displays) == -1)
        throw new TurnJsError('"' + display + '" is not a value for display');

      switch (display) {
        case "single":
          // Create a temporal page to use as folded page

          if (!data.pageObjs[0]) {
            this.turn("stop").css({ overflow: "hidden" });

            data.pageObjs[0] = $("<div />", { class: "page p-temporal" })
              .css({ width: this.width(), height: this.height() })
              .appendTo(this);
          }

          this.addClass("shadow");

          break;
        case "double":
          // Remove the temporal page

          if (data.pageObjs[0]) {
            this.turn("stop").css({ overflow: "" });
            data.pageObjs[0].remove();
            delete data.pageObjs[0];
          }

          this.removeClass("shadow");

          break;
      }

      data.display = display;

      if (currentDisplay) {
        var size = this.turn("size");
        turnMethods._movePages.call(this, 1, 0);
        this.turn("size", size.width, size.height).turn("update");
      }

      return this;
    }
  },

  // Gets and sets the direction of the flipbook

  direction: function (dir) {
    var data = this.data();

    if (dir === undefined) {
      return data.direction;
    } else {
      dir = dir.toLowerCase();

      if ($.inArray(dir, directions) == -1)
        throw new TurnJsError('"' + dir + '" is not a value for direction');

      if (dir == "rtl") {
        $(this).attr("dir", "ltr").css({ direction: "ltr" });
      }

      data.direction = dir;

      if (data.done) this.turn("size", $(this).width(), $(this).height());

      return this;
    }
  },

  // Detects animation

  animating: function () {
    return this.data().pageMv.length > 0;
  },

  // Gets the current activated corner

  corner: function () {
    var corner,
      page,
      data = this.data();

    for (page in data.pages) {
      if (has(page, data.pages))
        if ((corner = data.pages[page].flip("corner"))) {
          return corner;
        }
    }

    return false;
  },

  // Gets the data stored in the flipbook

  data: function () {
    return this.data();
  },

  // Disables and enables the effect

  disable: function (disable) {
    var page,
      data = this.data(),
      view = this.turn("view");

    data.disabled = disable === undefined || disable === true;

    for (page in data.pages) {
      if (has(page, data.pages))
        data.pages[page].flip(
          "disable",
          data.disabled ? true : $.inArray(parseInt(page, 10), view) == -1
        );
    }

    return this;
  },

  // Disables and enables the effect

  disabled: function (disable) {
    if (disable === undefined) {
      return this.data().disabled === true;
    } else {
      return this.turn("disable", disable);
    }
  },

  // Gets and sets the size

  size: function (width, height) {
    if (width === undefined || height === undefined) {
      return { width: this.width(), height: this.height() };
    } else {
      this.turn("stop");

      var page,
        prop,
        data = this.data(),
        pageWidth = data.display == "double" ? width / 2 : width;

      this.css({ width: width, height: height });

      if (data.pageObjs[0])
        data.pageObjs[0].css({ width: pageWidth, height: height });

      for (page in data.pageWrap) {
        if (!has(page, data.pageWrap)) continue;

        prop = turnMethods._pageSize.call(this, page, true);

        data.pageObjs[page].css({ width: prop.width, height: prop.height });
        data.pageWrap[page].css(prop);

        if (data.pages[page])
          data.pages[page].css({ width: prop.width, height: prop.height });
      }

      this.turn("resize");

      return this;
    }
  },

  // Resizes each page

  resize: function () {
    var page,
      data = this.data();

    if (data.pages[0]) {
      data.pageWrap[0].css({ left: -this.width() });
      data.pages[0].flip("resize", true);
    }

    for (page = 1; page <= data.totalPages; page++)
      if (data.pages[page]) data.pages[page].flip("resize", true);

    turnMethods._updateShadow.call(this);

    if (data.opts.autoCenter) this.turn("center");
  },

  // Removes an animation from the cache

  _removeMv: function (page) {
    var i,
      data = this.data();

    for (i = 0; i < data.pageMv.length; i++)
      if (data.pageMv[i] == page) {
        data.pageMv.splice(i, 1);
        return true;
      }

    return false;
  },

  // Adds an animation to the cache

  _addMv: function (page) {
    var data = this.data();

    turnMethods._removeMv.call(this, page);
    data.pageMv.push(page);
  },

  // Gets indexes for a view

  _view: function (page) {
    var data = this.data();

    page = page || data.page;

    if (data.display == "double")
      return page % 2 ? [page - 1, page] : [page, page + 1];
    else return [page];
  },

  // Gets a view

  view: function (page) {
    var data = this.data(),
      view = turnMethods._view.call(this, page);

    if (data.display == "double")
      return [
        view[0] > 0 ? view[0] : 0,
        view[1] <= data.totalPages ? view[1] : 0,
      ];
    else return [view[0] > 0 && view[0] <= data.totalPages ? view[0] : 0];
  },

  // Stops animations

  stop: function (ignore, animate) {
    if (this.turn("animating")) {
      var i,
        opts,
        page,
        data = this.data();

      if (data.tpage) {
        data.page = data.tpage;
        delete data["tpage"];
      }

      for (i = 0; i < data.pageMv.length; i++) {
        if (!data.pageMv[i] || data.pageMv[i] === ignore) continue;

        page = data.pages[data.pageMv[i]];
        opts = page.data().f.opts;

        page.flip("hideFoldedPage", animate);

        if (!animate) flipMethods._moveFoldingPage.call(page, false);

        if (opts.force) {
          opts.next = opts.page % 2 === 0 ? opts.page - 1 : opts.page + 1;
          delete opts["force"];
        }
      }
    }

    this.turn("update");

    return this;
  },

  // Gets and sets the number of pages

  pages: function (pages) {
    var data = this.data();

    if (pages) {
      if (pages < data.totalPages) {
        for (var page = data.totalPages; page > pages; page--)
          this.turn("removePage", page);
      }

      data.totalPages = pages;
      turnMethods._fitPage.call(this, data.page);

      return this;
    } else return data.totalPages;
  },

  // Checks missing pages

  _missing: function (page) {
    var data = this.data();

    if (data.totalPages < 1) return;

    var p,
      range = this.turn("range", page),
      missing = [];

    for (p = range[0]; p <= range[1]; p++) {
      if (!data.pageObjs[p]) missing.push(p);
    }

    if (missing.length > 0) this.trigger("missing", [missing]);
  },

  // Sets a page without effect

  _fitPage: function (page) {
    var data = this.data(),
      newView = this.turn("view", page);

    turnMethods._missing.call(this, page);

    if (!data.pageObjs[page]) return;

    data.page = page;

    this.turn("stop");

    for (var i = 0; i < newView.length; i++) {
      if (newView[i] && data.pageZoom[newView[i]] != data.zoom) {
        this.trigger("zoomed", [
          newView[i],
          newView,
          data.pageZoom[newView[i]],
          data.zoom,
        ]);

        data.pageZoom[newView[i]] = data.zoom;
      }
    }

    turnMethods._removeFromDOM.call(this);
    turnMethods._makeRange.call(this);
    turnMethods._updateShadow.call(this);
    this.trigger("turned", [page, newView]);
    this.turn("update");

    if (data.opts.autoCenter) this.turn("center");
  },

  // Turns the page

  _turnPage: function (page) {
    var current,
      next,
      data = this.data(),
      place = data.pagePlace[page],
      view = this.turn("view"),
      newView = this.turn("view", page);

    if (data.page != page) {
      var currentPage = data.page;

      if (trigger("turning", this, [page, newView]) == "prevented") {
        if (currentPage == data.page && $.inArray(place, data.pageMv) != -1)
          data.pages[place].flip("hideFoldedPage", true);

        return;
      }

      if ($.inArray(1, newView) != -1) this.trigger("first");
      if ($.inArray(data.totalPages, newView) != -1) this.trigger("last");
    }

    if (data.display == "single") {
      current = view[0];
      next = newView[0];
    } else if (view[1] && page > view[1]) {
      current = view[1];
      next = newView[0];
    } else if (view[0] && page < view[0]) {
      current = view[0];
      next = newView[1];
    }

    var optsCorners = data.opts.turnCorners.split(","),
      flipData = data.pages[current].data().f,
      opts = flipData.opts,
      actualPoint = flipData.point;

    turnMethods._missing.call(this, page);

    if (!data.pageObjs[page]) return;

    this.turn("stop");

    data.page = page;

    turnMethods._makeRange.call(this);

    data.tpage = next;

    if (opts.next != next) {
      opts.next = next;
      opts.force = true;
    }

    this.turn("update");

    flipData.point = actualPoint;

    if (flipData.effect == "hard")
      if (data.direction == "ltr")
        data.pages[current].flip("turnPage", page > current ? "r" : "l");
      else data.pages[current].flip("turnPage", page > current ? "l" : "r");
    else {
      if (data.direction == "ltr")
        data.pages[current].flip(
          "turnPage",
          optsCorners[page > current ? 1 : 0]
        );
      else
        data.pages[current].flip(
          "turnPage",
          optsCorners[page > current ? 0 : 1]
        );
    }
  },

  // Gets and sets a page

  page: function (page) {
    var data = this.data();

    if (page === undefined) {
      return data.page;
    } else {
      if (!data.disabled && !data.destroying) {
        page = parseInt(page, 10);

        if (page > 0 && page <= data.totalPages) {
          if (page != data.page) {
            if (!data.done || $.inArray(page, this.turn("view")) != -1)
              turnMethods._fitPage.call(this, page);
            else turnMethods._turnPage.call(this, page);
          }

          return this;
        } else {
          throw new TurnJsError("The page " + page + " does not exist");
        }
      }
    }
  },

  // Turns to the next view

  next: function () {
    return this.turn(
      "page",
      Math.min(
        this.data().totalPages,
        turnMethods._view.call(this, this.data().page).pop() + 1
      )
    );
  },

  // Turns to the previous view

  previous: function () {
    return this.turn(
      "page",
      Math.max(1, turnMethods._view.call(this, this.data().page).shift() - 1)
    );
  },

  // Shows a peeling corner

  peel: function (corner, animate) {
    var data = this.data(),
      view = this.turn("view");

    animate = animate === undefined ? true : animate === true;

    if (corner === false) {
      this.turn("stop", null, animate);
    } else {
      if (data.display == "single") {
        data.pages[data.page].flip("peel", corner, animate);
      } else {
        var page;

        if (data.direction == "ltr") {
          page = corner.indexOf("l") != -1 ? view[0] : view[1];
        } else {
          page = corner.indexOf("l") != -1 ? view[1] : view[0];
        }

        if (data.pages[page]) data.pages[page].flip("peel", corner, animate);
      }
    }

    return this;
  },

  // Adds a motion to the internal list
  // This event is called in context of flip

  _addMotionPage: function () {
    var opts = $(this).data().f.opts,
      turn = opts.turn;

    turnMethods._addMv.call(turn, opts.page);
  },

  // This event is called in context of flip

  _eventStart: function (e, opts, corner) {
    var data = opts.turn.data(),
      actualZoom = data.pageZoom[opts.page];

    if (e.isDefaultPrevented()) {
      turnMethods._updateShadow.call(opts.turn);
      return;
    }

    if (actualZoom && actualZoom != data.zoom) {
      opts.turn.trigger("zoomed", [
        opts.page,
        opts.turn.turn("view", opts.page),
        actualZoom,
        data.zoom,
      ]);

      data.pageZoom[opts.page] = data.zoom;
    }

    if (data.display == "single" && corner) {
      if (
        (corner.charAt(1) == "l" && data.direction == "ltr") ||
        (corner.charAt(1) == "r" && data.direction == "rtl")
      ) {
        opts.next = opts.next < opts.page ? opts.next : opts.page - 1;
        opts.force = true;
      } else {
        opts.next = opts.next > opts.page ? opts.next : opts.page + 1;
      }
    }

    turnMethods._addMotionPage.call(e.target);
    turnMethods._updateShadow.call(opts.turn);
  },

  // This event is called in context of flip

  _eventEnd: function (e, opts, turned) {
    var turn = opts.turn,
      dd = turn.data();

    if (turned) {
      var tpage = dd.tpage || dd.page;

      if (tpage == opts.next || tpage == opts.page) {
        delete dd.tpage;

        turnMethods._fitPage.call(turn, tpage || opts.next, true);
      }
    } else {
      turnMethods._removeMv.call(turn, opts.page);
      turnMethods._updateShadow.call(turn);
      turn.turn("update");
    }
  },

  // This event is called in context of flip

  _eventPressed: function (e) {
    var data = $(e.target).data().f,
      turn = data.opts.turn,
      turnData = turn.data();

    turnData.mouseAction = true;

    turn.turn("update");

    return (data.time = new Date().getTime());
  },

  // This event is called in context of flip

  _eventReleased: function (e, point) {
    var outArea,
      page = $(e.target),
      data = page.data().f,
      turn = data.opts.turn,
      turnData = turn.data();

    if (turnData.display == "single") {
      outArea =
        point.corner == "br" || point.corner == "tr"
          ? point.x < page.width() / 2
          : point.x > page.width() / 2;
    } else {
      outArea = point.x < 0 || point.x > page.width();
    }

    if (new Date().getTime() - data.time < 200 || outArea) {
      e.preventDefault();
      turnMethods._turnPage.call(turn, data.opts.next);
    }

    turnData.mouseAction = false;
  },

  // This event is called in context of flip

  _flip: function (e) {
    e.stopPropagation();

    var opts = $(e.target).data().f.opts;

    opts.turn.trigger("turn", [opts.next]);

    if (opts.turn.data().opts.autoCenter) {
      opts.turn.turn("center", opts.next);
    }
  },

  //
  _touchStart: function () {
    var data = this.data();
    for (var page in data.pages) {
      if (
        has(page, data.pages) &&
        flipMethods._eventStart.apply(data.pages[page], arguments) === false
      ) {
        return false;
      }
    }
  },

  //
  _touchMove: function () {
    var data = this.data();
    for (var page in data.pages) {
      if (has(page, data.pages)) {
        flipMethods._eventMove.apply(data.pages[page], arguments);
      }
    }
  },

  //
  _touchEnd: function () {
    var data = this.data();
    for (var page in data.pages) {
      if (has(page, data.pages)) {
        flipMethods._eventEnd.apply(data.pages[page], arguments);
      }
    }
  },

  // Calculate the z-index value for pages during the animation

  calculateZ: function (mv) {
    var i,
      page,
      nextPage,
      placePage,
      dpage,
      that = this,
      data = this.data(),
      view = this.turn("view"),
      currentPage = view[0] || view[1],
      total = mv.length - 1,
      r = { pageZ: {}, partZ: {}, pageV: {} },
      addView = function (page) {
        var view = that.turn("view", page);
        if (view[0]) r.pageV[view[0]] = true;
        if (view[1]) r.pageV[view[1]] = true;
      };

    for (i = 0; i <= total; i++) {
      page = mv[i];
      nextPage = data.pages[page].data().f.opts.next;
      placePage = data.pagePlace[page];
      addView(page);
      addView(nextPage);
      dpage = data.pagePlace[nextPage] == nextPage ? nextPage : page;
      r.pageZ[dpage] = data.totalPages - Math.abs(currentPage - dpage);
      r.partZ[placePage] = data.totalPages * 2 - total + i;
    }

    return r;
  },

  // Updates the z-index and display property of every page

  update: function () {
    var page,
      data = this.data();

    if (this.turn("animating") && data.pageMv[0] !== 0) {
      // Update motion

      var p,
        fixed,
        pos = this.turn("calculateZ", data.pageMv),
        corner = this.turn("corner"),
        actualView = this.turn("view"),
        newView = this.turn("view", data.tpage);

      for (page in data.pageWrap) {
        if (!has(page, data.pageWrap)) continue;

        fixed = data.pageObjs[page].hasClass("fixed");

        data.pageWrap[page].css({
          display: pos.pageV[page] || fixed ? "" : "none",
          zIndex:
            (data.pageObjs[page].hasClass("hard")
              ? pos.partZ[page]
              : pos.pageZ[page]) || (fixed ? -1 : 0),
        });

        if ((p = data.pages[page])) {
          p.flip("z", pos.partZ[page] || null);

          if (pos.pageV[page]) p.flip("resize");

          if (data.tpage) {
            // Is it turning the page to `tpage`?

            p.flip("hover", false).flip(
              "disable",
              $.inArray(parseInt(page, 10), data.pageMv) == -1 &&
                page != newView[0] &&
                page != newView[1]
            );
          } else {
            p.flip("hover", corner === false).flip(
              "disable",
              page != actualView[0] && page != actualView[1]
            );
          }
        }
      }
    } else {
      // Update static pages

      for (page in data.pageWrap) {
        if (!has(page, data.pageWrap)) continue;

        var pageLocation = turnMethods._setPageLoc.call(this, page);

        if (data.pages[page]) {
          data.pages[page]
            .flip("disable", data.disabled || pageLocation != 1)
            .flip("hover", true)
            .flip("z", null);
        }
      }
    }

    return this;
  },

  // Updates the position and size of the flipbook's shadow

  _updateShadow: function () {
    var view,
      view2,
      shadow,
      data = this.data(),
      width = this.width(),
      height = this.height(),
      pageWidth = data.display == "single" ? width : width / 2;

    view = this.turn("view");

    if (!data.shadow) {
      data.shadow = $("<div />", {
        class: "shadow",
        css: divAtt(0, 0, 0).css,
      }).appendTo(this);
    }

    for (var i = 0; i < data.pageMv.length; i++) {
      if (!view[0] || !view[1]) break;

      view = this.turn("view", data.pages[data.pageMv[i]].data().f.opts.next);
      view2 = this.turn("view", data.pageMv[i]);

      view[0] = view[0] && view2[0];
      view[1] = view[1] && view2[1];
    }

    if (!view[0]) shadow = data.direction == "ltr" ? 1 : 2;
    else if (!view[1]) shadow = data.direction == "ltr" ? 2 : 1;
    else shadow = 3;

    switch (shadow) {
      case 1:
        data.shadow.css({
          width: pageWidth,
          height: height,
          top: 0,
          left: pageWidth,
        });
        break;
      case 2:
        data.shadow.css({
          width: pageWidth,
          height: height,
          top: 0,
          left: 0,
        });
        break;
      case 3:
        data.shadow.css({
          width: width,
          height: height,
          top: 0,
          left: 0,
        });
        break;
    }
  },

  // Sets the z-index and display property of a page
  // It depends on the current view

  _setPageLoc: function (page) {
    var data = this.data(),
      view = this.turn("view"),
      loc = 0;

    if (page == view[0] || page == view[1]) loc = 1;
    else if (
      (data.display == "single" && page == view[0] + 1) ||
      (data.display == "double" && page == view[0] - 2) ||
      page == view[1] + 2
    )
      loc = 2;

    if (!this.turn("animating"))
      switch (loc) {
        case 1:
          data.pageWrap[page].css({
            zIndex: data.totalPages,
            display: "",
          });
          break;
        case 2:
          data.pageWrap[page].css({
            zIndex: data.totalPages - 1,
            display: "",
          });
          break;
        case 0:
          data.pageWrap[page].css({
            zIndex: 0,
            display: data.pageObjs[page].hasClass("fixed") ? "" : "none",
          });
          break;
      }

    return loc;
  },

  // Gets and sets the options

  options: function (options) {
    if (options === undefined) {
      return this.data().opts;
    } else {
      var data = this.data();

      // Set new values

      $.extend(data.opts, options);

      // Set pages

      if (options.pages) this.turn("pages", options.pages);

      // Set page

      if (options.page) this.turn("page", options.page);

      // Set display

      if (options.display) this.turn("display", options.display);

      // Set direction

      if (options.direction) this.turn("direction", options.direction);

      // Set size

      if (options.width && options.height)
        this.turn("size", options.width, options.height);

      // Add event listeners

      if (options.when)
        for (var eventName in options.when)
          if (has(eventName, options.when)) {
            this.unbind(eventName).bind(eventName, options.when[eventName]);
          }

      return this;
    }
  },
};
